<template>
  <div class="virtual-container">
    <div>
      <input type="text" v-model.number="dataLength" />
      <span>条 Height:{{ scrollBarHeight }}</span>
    </div>
    <div class="phantom" ref="scroll" @scroll="onScroll">
      <!-- 列表总高 -->
      <ul :style="{ height: scrollBarHeight + 'px' }">
        <!-- 列表偏移量 -->
        <ItemView
          v-for="item in visibleList"
          :index="item.index"
          :item="item"
          :key="item.id"
          @update-height="updateItemHeight"
          :style="{ transform: `translate3d(0,${scrollTop}px,0)` }"
        />
      </ul>
    </div>
  </div>
</template>

<script>
import * as utils from "@/utils/index.js";
import ItemView from "./virtual-item.vue";

const lnSum = 5000;

export default {
  name: "virtual-container",
  components: {
    ItemView,
  },
  data() {
    return {
      estimatedItemHeight: 30, // 每一项假定的高度
      visibleCount: 10, // 可视窗口展示项
      dataLength: lnSum, // 模拟列表项
      startIndex: 0, // 截取数组的 起始 索引
      endIndex: 10, // 截取数组的 结束 索引
      scrollTop: 0, // 距离顶部的偏移量
      scrollBarHeight: 0, // 虚拟列表高度
      bufferItemCount: 4, // 缓冲加载项
      dataList: utils.listCreate(lnSum), // 数据列表
      itemHeightCache: [], // 每一项高度
      itemTopCache: [], // 每一项距顶部的实际高度
    };
  },
  computed: {
    // 截取要展示的数据
    visibleList() {
      return this.dataList.slice(
        this.startIndex,
        this.endIndex + this.bufferItemCount
      );
    },
  },
  watch: {},
  created() {
    this.generateEstimatedItemData();
  },
  methods: {
    generateEstimatedItemData() {
      const estimatedTotalHeight = this.dataList.reduce(
        (pre, current, index) => {
          // 给每一项一个虚拟高度
          this.itemHeightCache[index] = this.estimatedItemHeight;
          // 给每一项距顶部的虚拟高度
          this.itemTopCache[index] =
            index === 0
              ? 0
              : this.itemTopCache[index - 1] + this.estimatedItemHeight;
          return pre + this.estimatedItemHeight;
        },
        0
      );
      // 列表总高
      this.scrollBarHeight = estimatedTotalHeight;
    },
    updateItemHeight({ index, height }) {
      // dom元素加载后得到实际高度 重新赋值回去
      this.itemHeightCache[index] = height;
      // 重新确定列表的实际总高度
      this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
        return pre + current;
      }, 0);
      let newItemTopCache = [0];
      for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
        // 虚拟每项距顶部高度 + 实际每项高度
        newItemTopCache[i] =
          this.itemTopCache[i - 1] + this.itemHeightCache[i - 1];
      }
      // 获得每一项距顶部的实际高度
      this.itemTopCache = newItemTopCache;
    },
    // 获取渲染项起始索引
    getStartIndex(scrollTop) {
      // 每一项距顶部的距离
      let arr = this.itemTopCache;
      let index = -1;
      let left = 0,
        right = arr.length - 1,
        mid = Math.floor((left + right) / 2);
      // 判断 有可循环项时进入
      while (right - left > 1) {
        /*
        二分法：拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
        arr[mid] 为虚拟列高度的中间项 
        不断while 循环，利用二分之一将数组分割，减小搜索范围
        直到最终定位到 目标index 值
      */
        // 目标数在左侧
        if (scrollTop < arr[mid]) {
          right = mid;
          mid = Math.floor((left + right) / 2);
        } else if (scrollTop > arr[mid]) {
          // 目标数在右侧
          left = mid;
          mid = Math.floor((left + right) / 2);
        } else {
          index = mid;
          return index;
        }
      }
      index = left;
      return index;
    },
    onScroll() {
      console.log(this.$refs.scroll.scrollTop);
      const scrollTop = this.$refs.scroll.scrollTop;
      console.log("scrollTop", scrollTop);
      let startIndex = this.getStartIndex(scrollTop);
      // 如果是奇数开始，就取其前一位偶数
      if (startIndex % 2 !== 0) {
        this.startIndex = startIndex - 1;
      } else {
        this.startIndex = startIndex;
      }
      this.endIndex = this.startIndex + this.visibleCount;
      this.scrollTop = this.itemTopCache[this.startIndex] || 0;
    },
  },
};
</script>

<style lang="scss" scoped>
.virtual-container {
  .phantom {
    border: solid 1px #eee;
    margin-top: 10px;
    height: 600px;
    overflow: auto;
  }

  ul {
    background: #ccc;
    list-style: none;
    padding: 0;
    margin: 0;
    li {
      outline: solid 1px #fff;
      &:nth-child(2n) {
        background: #fff;
      }
    }
  }
}
</style>
